Keras é um pacote que oferece funções para o estudo sobre modelos abordando deep learning no R.
Esse tutorial terá como foco o MLP (Multi-Layer Perceptron), um tipo de Rede Neural Artificial que possui pelo menos três camadas internas compostas por nós (neurônios).
Primeiro, importaremos o pacote devtools para poder instalar os pacotes que serão necessários:
library(devtools)
Em seguida, são importados o keras e tensorflow, para isso é necessário tê-los instalados:
# install_github("rstudio/tensorflow")
# install_github("rstudio/keras")
library(keras)
library(tensorflow)
# install_keras()
Os dados podem ser carregados de 3 maneiras diferentes:
Utilizando os datasets do keras:
mnist <- dataset_mnist()
cifar10 <- dataset_cifar10()
imdb <- dataset_imdb()
Criando nosso próprio dataset com dados aleatórios a partir da função matrix():
dados <- matrix(rexp(1000*784), nrow = 1000, ncol = 784)
labels <- matrix(round(runif(1000 * 10, min = 0, max = 9)),
nrow = 1000, ncol = 10)
Importando de um arquivo CSV (ou outro formato):
# Carregando o arquivo na variável iris:
iris <- read.csv(url("http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"), header = FALSE)
# Cabeçalho do dataset
head(iris)
## V1 V2 V3 V4 V5
## 1 5.1 3.5 1.4 0.2 Iris-setosa
## 2 4.9 3.0 1.4 0.2 Iris-setosa
## 3 4.7 3.2 1.3 0.2 Iris-setosa
## 4 4.6 3.1 1.5 0.2 Iris-setosa
## 5 5.0 3.6 1.4 0.2 Iris-setosa
## 6 5.4 3.9 1.7 0.4 Iris-setosa
# Estrutura
str(iris)
## 'data.frame': 150 obs. of 5 variables:
## $ V1: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
## $ V2: num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
## $ V3: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
## $ V4: num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
## $ V5: Factor w/ 3 levels "Iris-setosa",..: 1 1 1 1 1 1 1 1 1 1 ...
# Dimensões
dim(iris)
## [1] 150 5
Utilizaremos esse dataset que contém dados sobre as dimensões de flores de íris e suas respectivas classificações. Mais detalhes em: http://archive.ics.uci.edu/ml/datasets/Iris
É importante saber que todas as flores possuem sépalas e pétalas. Sépalas tem tipicamente um tom esverdeado, enquanto as pétalas são coloridas. Isso é diferente nas íris, como pode ser visto nas imagens:
Adicionaremos nomes para as colunas com a função names() e utilizaremos a função plot() para visualizar a correlação entre o comprimento e a largura das pétalas:
# Atribuindo nomes às colunas
names(iris) <- c("Sepala.Comprimento", "Sepala.Largura", "Petala.Comprimento", "Petala.Largura", "Especie")
# Plotando o gráfico de correlação
plot(iris$Petala.Comprimento,
iris$Petala.Largura,
pch = 21,
bg = c("red", "green3", "blue")[unclass(iris$Especie)],
xlab = "Comprimento da Petala",
ylab = "Largura da Petala")
# Correlação geral entre os dois atributos
cor(iris$Petala.Comprimento, iris$Petala.Largura)
## [1] 0.9627571
A função unclass() foi usada para mapear os nomes das espécies para valores numéricos (1, 2 e 3).
Pelo gráfico, é possível observar que há uma correlação significativa entre o comprimento e a largura da pétala e seu valor é de 0.9627571.
Precisamos verificar os outros atributos, para isso utilizamos a função cor() com todos os atributos. Além disso, utilizamos a função corrplot() do pacote de mesmo nome para ter uma melhor visão das correlações:
# Salvando a correção geral em M
M <- cor(iris[,1:4])
# Importando o pacote corrplot
library(corrplot)
# Plotando o gráfico de correlações com o método de círculos
corrplot(M, method = "circle")
Antes de construírmos o modelo, é necessário termos certeza de que os dados estão limpos, normalizados e divididos entre treino e teste.
Como estamos fazendo uso de um dataset do UCI Machine Learning Repository, podemos esperar que esses dados já estejam prontos para uso, ou seja, limpos e normalizados. Checaremos isso a seguir.
Iremos utilizar a função normalize() do keras. Para isso, precisamos ter os dados dispostos em uma matriz:
# Convertendo os nomes das espécies em valores numéricos
iris[,5] <- as.numeric(iris[,5]) -1
# Convertendo os dados em uma matriz
iris <- as.matrix(iris)
# Excluindo os nomes das colunas
dimnames(iris) <- NULL
# Normalizando os dados
irisNormalizado <- normalize(iris[,1:4])
# Mostrando o sumário
summary(iris)
## V1 V2 V3 V4
## Min. :4.300 Min. :2.000 Min. :1.000 Min. :0.100
## 1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st Qu.:0.300
## Median :5.800 Median :3.000 Median :4.350 Median :1.300
## Mean :5.843 Mean :3.054 Mean :3.759 Mean :1.199
## 3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.800
## Max. :7.900 Max. :4.400 Max. :6.900 Max. :2.500
## V5
## Min. :0
## 1st Qu.:0
## Median :1
## Mean :1
## 3rd Qu.:2
## Max. :2
summary(irisNormalizado)
## V1 V2 V3 V4
## Min. :0.6539 Min. :0.2384 Min. :0.1678 Min. :0.01473
## 1st Qu.:0.7153 1st Qu.:0.3267 1st Qu.:0.2509 1st Qu.:0.04873
## Median :0.7549 Median :0.3544 Median :0.5364 Median :0.16415
## Mean :0.7516 Mean :0.4048 Mean :0.4550 Mean :0.14096
## 3rd Qu.:0.7884 3rd Qu.:0.5252 3rd Qu.:0.5800 3rd Qu.:0.19753
## Max. :0.8609 Max. :0.6071 Max. :0.6370 Max. :0.28042
Precisamos separar nosso dataset em treino e teste. Para isso, definimos um peso para cada uma das duas partições utilizando a função sample():
# Define o tamanho das amostras
ind <- sample(2, nrow(iris), replace = TRUE, prob = c(0.67, 0.33))
# Divide os dados
iris.treino <- iris[ind==1, 1:4]
iris.teste <- iris[ind==2, 1:4]
# Divide o atributo de classificação
iris.treinoalvo <- iris[ind==1, 5]
iris.testealvo <- iris[ind==2, 5]
Utilizaremos a função to_categorical() para converter os arrays com o atributo alvo para uma matriz de booleanos, que indica a qual classe pertence cada dado:
iris.treinoRotulos <- to_categorical(iris.treinoalvo)
iris.testeRotulos <- to_categorical(iris.testealvo)
Nosso objetivo é classificar flores de íris como versicolor, setosa ou virginica. Esse problema é conhecido como um perceptron multi-camadas, em que temos camadas internas totalmente conectadas com uma função de ativação, que nesse caso será a ReLU. Além disso, utilizaremos a função softmax para a camada de saída, onde teremos valores na faixa de 0 a 1.
# Iniciando o modelo sequencial
modelo <- keras_model_sequential()
# Adicionando as camadas internas e de saída
modelo %>% layer_dense(units = 8, activation = "relu",
input_shape = c(4)) %>% layer_dense(units = 3, activation = "softmax")
Visualizando o modelo:
# Sumário
summary(modelo)
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## dense_1 (Dense) (None, 8) 40
## ___________________________________________________________________________
## dense_2 (Dense) (None, 3) 27
## ===========================================================================
## Total params: 67
## Trainable params: 67
## Non-trainable params: 0
## ___________________________________________________________________________
# Configuração
get_config(modelo)
## [{'class_name': 'Dense', 'config': {'name': 'dense_1', 'trainable': True, 'batch_input_shape': (None, 4), 'dtype': 'float32', 'units': 8, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}, {'class_name': 'Dense', 'config': {'name': 'dense_2', 'trainable': True, 'units': 3, 'activation': 'softmax', 'use_bias': True, 'kernel_initializer': {'class_name': 'VarianceScaling', 'config': {'scale': 1.0, 'mode': 'fan_avg', 'distribution': 'uniform', 'seed': None}}, 'bias_initializer': {'class_name': 'Zeros', 'config': {}}, 'kernel_regularizer': None, 'bias_regularizer': None, 'activity_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}}]
# Configuração das camadas
get_layer(modelo, index = 1)
## <keras.layers.core.Dense>
# Camadas
modelo$layers
## [[1]]
## <keras.layers.core.Dense>
##
## [[2]]
## <keras.layers.core.Dense>
# Entradas
modelo$inputs
## [[1]]
## Tensor("dense_1_input:0", shape=(?, 4), dtype=float32)
# Saídas
modelo$outputs
## [[1]]
## Tensor("dense_2/Softmax:0", shape=(?, 3), dtype=float32)
# Compilando o modelo
modelo %>% compile(
loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy'
)
Os parâmetros loss e optimizer são necessários para treinar o modelo. O primeiro representa a função de perda utilizada, enquanto o segundo significa o algoritmo de otimização.
O próximo passo é treinar o modelo com os dados de treino, por 200 épocas, em lotes de 5 amostras:
# Treinando o modelo e guardando em 'treinamento'
treinamento <- modelo %>% fit(
iris.treino,
iris.treinoRotulos,
epochs = 200,
batch_size = 5,
validation_split = 0.2
)
O treino pode ser visto abaixo com detalhes:
# Plotando o gráfico
plot(treinamento)
Os gráficos acima parecem um pouco confusos. Temos um representando a perda e outro a acurácia. Veremos de forma mais clara abaixo:
# Plotando a perda dos dados de treino
plot(treinamento$metrics$loss, main="Perda", xlab = "Epocas", ylab="Perda", col="blue", type="l")
# Plotando a perda dos dados de teste
lines(treinamento$metrics$val_loss, col="green")
# Adicionando legendas
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
# Plot the accuracy of the training data
plot(treinamento$metrics$acc, main="Acurácia", xlab = "Epocas", ylab="Acurácia", col="blue", type="l")
# Plot the accuracy of the validation data
lines(treinamento$metrics$val_acc, col="green")
# Add Legend
legend("bottomright", c("train","test"), col=c("blue", "green"), lty=c(1,1))
Importante:
Agora que temos nosso modelo compilado e treinado, podemos fazer a predição dos dados de teste a partir da função predict:
# Predição das classes do dataset de teste
classes <- modelo %>% predict_classes(iris.teste, batch_size = 128)
# Matriz de confusão para verificar as predições
table(iris.testealvo, classes)
## classes
## iris.testealvo 0 1 2
## 0 13 0 0
## 1 0 14 1
## 2 0 3 11
Podemos ver a partir de uma pontuação como nosso modelo se saiu:
# Guardando a avaliação do modelo
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)
# Imprimindo o resultado
print(score)
## $loss
## [1] 0.1946697
##
## $acc
## [1] 0.9047619
Na maioria das vezes precisamos fazer ajustes para o modelo a partir dos resultados obtidos. Faremos três ajustes a fim de melhorar nosso modelo:
Veremos o que acontecerá caso adicionássemos mais uma camada ao modelo:
# Inicializando o modelo sequencial
modelo <- keras_model_sequential()
# Adicionando camadas
modelo %>%
layer_dense(units = 8, activation = 'relu', input_shape = c(4)) %>%
layer_dense(units = 5, activation = 'relu') %>%
layer_dense(units = 3, activation = 'softmax')
# Compilando
modelo %>% compile(
loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy'
)
# Treinando
treinamento <- modelo %>% fit(
iris.treino, iris.treinoRotulos,
epochs = 200, batch_size = 5,
validation_split = 0.2
)
# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)
# Imprimindo o score
print(score)
## $loss
## [1] 0.1538553
##
## $acc
## [1] 0.9285714
Visualizando a perda e acurácia do novo modelo:
# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
Veremos o efeito de adicionar mais nós nas camadas internas do nosso modelo:
# Inicializando o modelo sequencial
modelo <- keras_model_sequential()
# Adicionando camadas
modelo %>%
layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>%
layer_dense(units = 3, activation = 'softmax')
# Compilando
modelo %>% compile(
loss = 'categorical_crossentropy',
optimizer = 'adam',
metrics = 'accuracy'
)
# Treinando
treinamento <- modelo %>% fit(
iris.treino, iris.treinoRotulos,
epochs = 200, batch_size = 5,
validation_split = 0.2
)
# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)
# Imprimindo o score
print(score)
## $loss
## [1] 0.1591196
##
## $acc
## [1] 0.9285714
É importante saber que aumentar a quantidade de nós internos nem sempre significa melhorar a performance do modelo, podendo gerar overfitting. No nosso caso, com um dataset pequeno, o ideal é usar uma rede com poucos nós.
Visualizando o efeito:
# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
Tentaremos utilizar outro algoritmo de otimização (SGD), até agora estávamos usando o ADAM:
# Inicializando o modelo sequencial
modelo <- keras_model_sequential()
# Adicionando camadas
modelo %>%
layer_dense(units = 28, activation = 'relu', input_shape = c(4)) %>%
layer_dense(units = 3, activation = 'softmax')
# Definindo o algoritmo com learning rate 0.01 (taxa de aprendizado)
sgd <- optimizer_sgd(lr = 0.01)
# Usando o algoritmo para compilar o modelo
modelo %>% compile(optimizer = sgd,
loss='categorical_crossentropy',
metrics='accuracy')
# Treinando
treinamento <- modelo %>% fit(
iris.treino, iris.treinoRotulos,
epochs = 200, batch_size = 5,
validation_split = 0.2
)
# Avaliando
score <- modelo %>% evaluate(iris.teste, iris.testeRotulos, batch_size = 128)
# Iprime as métricas de perda e acurácia
print(score)
## $loss
## [1] 0.1831878
##
## $acc
## [1] 0.9047619
Além de alterar o algoritmo de otimização, é possível também alterar a taxa de aprendizado do algoritmo. Essa é uma das técnicas mais comuns de ajuste de modelo.
Veremos os efeitos dessa mudança:
# Plotando a perda
plot(treinamento$metrics$loss, main="Perda", xlab = "epocas", ylab="perda", col="blue", type="l")
lines(treinamento$metrics$val_loss, col="green")
legend("topright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
# Plotando a acurácia
plot(treinamento$metrics$acc, main="Acurácia", xlab = "epocas", ylab="acuracia", col="blue", type="l")
lines(treinamento$metrics$val_acc, col="green")
legend("bottomright", c("treino","teste"), col=c("blue", "green"), lty=c(1,1))
Podemos salvar nosso modelo para utilizá-lo futuramente:
# Salvando o modelo
save_model_hdf5(modelo, "meu_modelo.h5")
# Carregando um modelo salvo
modelo <- load_model_hdf5("meu_modelo.h5")
É possível também salvar os pesos do modelo:
save_model_weights_hdf5(modelo, "pesos_modelo.h5")
modelo %>% load_model_weights_hdf5("pesos_modelo.h5")
Também podemos exportar o modelo para JSON ou YAML:
json_string <- model_to_json(modelo)
modelo <- model_from_json(json_string)
yaml_string <- model_to_yaml(modelo)
modelo <- model_from_yaml(yaml_string)